JS+ES6面试题总结
总结了最近的一些面试题和之前的知识点
进阶(多看)
EventLoop 事件循环
JS是单线程的,为了防止一个函数执行时间过长阻塞后面的代码,所以会先将同步代码压入执行栈中,依次执行,将异步代码推入异步队列,异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列。微任务队列的代表就是,Promise.then,MutationObserver,宏任务的话就是 setImmediate setTimeout setInterval
首先,整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为同步任务、异步任务两部分
同步任务会直接进入主线程依次执行(执行栈)
异步任务会再分为宏任务和微任务
宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时(例如延时器,或者异步请求,dom监听),Event Table会将这个函数移到Event Queue中
微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中
当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务(微任务队列清空后,进入宏任务队列,取队列的第一项任务放入Stack(栈)中执行,执行完成后,查看微任务队列是否有任务,有的话,清空微任务队列)
上述过程会不断重复(循环),这就是Event Loop,比较完整的事件循环
JS运行的环境。一般为浏览器或者Node。 在浏览器环境中,有JS 引擎线程和渲染线程,且两个线程互斥。 Node环境中,只有JS 线程。 不同环境执行机制有差异,不同任务进入不同Event Queue队列。 当主程结束,先执行准备好微任务,然后再执行准备好的宏任务,一个轮询结束。
当一个宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完(宏任务 -> 微任务 -> GUI渲染 -> 宏任务 -> …) (也就是当前)
你怎么理解执行栈的?
执行上下文的类型
JavaScript 中有三种执行上下文类型。
全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。
执行栈
执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO先进后出数据结构的栈,被用来存储代码运行时创建的所有执行上下文。
当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。
引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。
掘金
怎么创建执行上下文?
- this 值的决定,即我们所熟知的 This 绑定。
- 创建词法环境组件。
- 创建变量环境组件。
- eventLoop 是由JS的宿主环境(浏览器)来实现的;
- 浏览器中的任务源(task):
- 宏任务(macrotask):宿主环境提供的,比如浏览器ajax、setTimeout、setInterval、setTmmediate(只兼容ie)、script、2. 2.requestAnimationFrame、messageChannel、UI渲染、一些浏览器api
- 微任务(microtask):语言本身提供的,比如**promise.thenthen、queueMicrotask(基于then)、mutationObserver(浏览器提供)**、messageChannel 、mutationObersve
博客:这一次,彻底弄懂 JavaScript 执行机制
传送门 ☞ # 宏任务和微任务
promise 原理 (回调函数+状态处理队列)
promise 的本质是回调函数,then 方法的本质是依赖收集,它把 fulfilled 状态要执行的回调函数放在一个队列, rejected 状态要执行的回调函数放在另一个队列。待 promise 从 pending 变为 fulfilled/rejected 状态后,把相应队列的所有函数,执行一遍。
如何解决异步回调地狱|| 异步编程的实现方式?
promise、generator、async/await
js 中的异步机制可以分为以下几种:
- 第一种最常见的是使用回调函数的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
2. 第二种是 Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确
从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。
Promise 异步操作有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。除了异步操作的结果,任何其他操作都无法改变这个状态。
Promise 对象只有:从 pending 变为 fulfilled 和从 pending 变为 rejected 的状态改变。只要处于 fulfilled 和 rejected ,状态就不会再变了即 resolved(已定型)。
语法:
1 |
|
状态的缺点:
无法取消 Promise ,一旦新建它就会立即执行,无法中途取消。
如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部
当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
generator
第四种是使用 async 函数的形式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候, 如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此我们可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。
博客:
promise /generotor /await (await后面接收的是什么)
then 方法
then是实例状态发生改变时的回调函数,第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数
then方法返回的是一个新的Promise实例,也就是promise能链式书写的原因
在 JavaScript 事件队列的当前运行完成之前,回调函数永远不会被调用。
promise.then 返回的新的promise状态与值是怎样决定的?
1 |
|
结论:
1、通过return 返回一个非promise的值,则新promise的状态fulfilled,值为return 的值
2、不做任何处理(不return == return undefined),所以根据结论1新promise的状态为fulfilled,值为undefined
3、通过throw主动抛出错误或者代码出现错误,则promise的状态为rejected,值为throw的值
4、通过return 返回一个promise对象,则新promise就是return的promsie
- 可根据以上代码,挨个取消注释并在控制台打印promise2查看
catch
catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数
finally()
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作
参考
正常情况下,async中的await命令是一个Promise对象,返回该对象的结果。
但如果不是Promise对象的话,就会直接返回对应的值,相当于Promise.resolve()
如果在async函数中抛出了错误,则终止错误结果,不会继续向下执行。
Promise.all、Promise.race、Promise.allSettled 分别怎么用?
Promise.all方法
简而言之:Promise.all( ).then( )适用于处理多个异步任务,且所有的异步任务都得到结果时的情况。
promise1和promise2都成功才会调用success1
比如:用户点击按钮,会弹出一个弹出对话框,对话框中有两部分数据呈现,这两部分数据分别是不同的后端接口获取的数据。
弹框弹出后的初始情况下,就让这个弹出框处于数据加载中的状态,当这两部分数据都从接口获取到的时候,才让这个数据加载中状态消失。让用户看到这两部分的数据。
Promise.race方法
Promise.race赛跑机制,只认第一名
promise1和promise2只要有一个成功就会调用success1
Promise.race其实使用的并不多,如果真要使用。我们可以提出这样一个需求:
比如:点击按钮发请求,当后端的接口超过一定时间,假设超过三秒,没有返回结果,我们就提示用户请求超时
Promise.allSettled
Promise.allSettled(promises) 允许您并行执行互相独立的promise, 并每一个promise执行之后的状态(已完成或拒绝)存放在数组中。
当您需要执行并行和独立的异步操作并获取到异步操作的所有结果时,Promise.allSettled(…) 非常有用,即使某些异步操作可能会失败。
什么是作用域
简单来说,作用域 指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限。
javascript 中大部分情况下,只有两种作用域类型:
- 全局作用域:全局作用域为程序的最外层作用域,一直存在。
- 函数作用域:函数作用域只有函数被定义时才会创建,包含在父级函数作用域 / 全局作用域内。
由于作用域的限制,每段独立的执行代码块只能访问自己作用域和外层作用域中的变量,无法访问到内层作用域的变量。
作用域链
当可执行代码内部访问变量时,会先查找本地作用域,如果找到目标变量即返回,否则会去父级作用域继续查找…一直找到全局作用域。我们把这种作用域的嵌套机制,称为 作用域链。
面试官:说说作用域和闭包吧
闭包是什么?有什么优点和缺点? (作用域&&函数套函数,内部函数对外部函数变量的引用会产生闭包)
解答:闭包是指有权访问另一个函数作用域中的变量的函数。
优点:① 能够读取函数内部的变量;②让这些变量一直存在于内存中,不会在调用结束后被垃圾回收机制回收;3.对变量进行保护,防止全局污染
缺点:由于闭包会使用函数中的变量存在在内存中,内存消耗很大,所以不能滥用闭包;解决的办法是退出函数之前,将不使用的局部变量删除;
博客:
闭包的使用
- for循环+setTimeout中i访问的是全局变量,使用闭包保存变量
原因是,for循环在主线程内,setTimeout是异步方法,在任务队列里面,只有主线程执行完后,任务队列才执行,此时i的值已经是5,所以得到结果是5个5
2. 防抖节流
防抖&&节流
介绍节流防抖原理、区别以及应用
防抖:多次触发事件,事件处理函数只能执行一次,并且是在触发操作结束时执行。也就是说,当一个事件被触发准备执行事件函数前,会等待一定的时间(这时间是码农自己去定义的,比如 1 秒),如果没有再次被触发,那么就执行,如果被触发了,那就本次作废,重新从新触发的时间开始计算,并再次等待 1 秒,直到能最终执行 ( clearTimeout(timeout) )
节流:事件触发后,规定时间内,事件处理函数不能再次被调用。也就是说在规定的时间内,函数只能被调用一次,且是最先被触发调用的那次
函数执行的前提条件是开关打开,持续触发时,持续关闭开关,等到setTimeout到时间了,再把开关打开,函数就会执行了。
使用场景
防抖:搜索框搜索输入,并在输入完以后自动搜索、手机号,邮箱验证输入检测、窗口大小 resize 变化后,再重新渲染。
节流:滚动加载更多、搜索框搜的索联想功能、高频点击、表单重复提交……
函数的防抖与节流 _
匿名函数的作用
1、通过匿名函数可以实现闭包(必须掌握的知识点)
2、模拟块级作用域,减少全局变量。执行完匿名函数,存储在内存中相对应的变量会被销毁,使用块级作用域,会大大降低命名冲突的问题,不必担心搞乱全局作用域了。
JS垃圾回收机制 (GC)
- 项目中,如果存在大量不被释放的内存(堆/栈/上下文),页面性能会变得很慢。当某些代码操作不能被合理释放,就会造成内存泄漏。我们尽可能减少使用闭包,因为它会消耗内存。
- 浏览器垃圾回收机制/内存回收机制:浏览器的Javascript具有自动垃圾回收机制(GC:Garbage Collecation),垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。
标记清除:在js中,最常用的垃圾回收机制是标记清除:当变量进入上下文,比如在函数内声明了一个变量时,这个变量就会被加上存在于上下文中的标记,而在上下文中的变量,逻辑上讲永远不应该释放他们的内存,因为只要在上下文中的代码运行了,就可能用到它们,当变量离开上下文中的时候,也会被加上离开上下文的标记。(上下文中的变量都访问不到他们了)垃圾回收器会销毁那些带离开上下文标记的值并回收它们所占用的内存空间。谷歌浏览器:“查找引用”,浏览器不定时去查找当前内存的引用,如果没有被占用了,浏览器会回收它;如果被占用,就不能回收。
过程
- 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
- 然后从各个根对象开始遍历,把不是垃圾的节点改成1
- 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
- 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收
优点 :
标记清除算法的优点只有一个,那就是实现比较简单,打标记也无非打与不打两种情况,这使得一位二进制位(0和1)就可以为其标记,非常简单
缺点
标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了 内存碎片,并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的
IE浏览器:“引用计数法”,当前内存被占用一次,计数累加1次,移除占用就减1,减到0时,浏览器就回收它。
缺点
引用计数的缺点想必大家也都很明朗了,首先它需要一个计数器,而此计数器需要占很大的位置,因为我们也不知道被引用数量的上限,还有就是无法解决循环引用无法回收的问题,这也是最严重的
- 优化手段:内存优化 ; 手动释放:取消内存的占用即可。(1)堆内存:fn = null 【null:空指针对象】(2)栈内存:把上下文中,被外部占用的堆的占用取消即可。
- 内存泄漏在 JS 中,常见的内存泄露主要有 4 种,全局变量、闭包、DOM 元素的引用、定时器
V8对GC的优化 (待)
- 分代式垃圾回收
V8 的垃圾回收策略主要基于分代式垃圾回收机制,V8 中将堆内存分为新生代和老生代两区域,采用不同的垃圾回收器也就是不同的策略管理垃圾回收
新生代的对象为存活时间较短的对象,简单来说就是新产生的对象,通常只支持 1~8M 的容量,而老生代的对象为存活事件较长或常驻内存的对象,简单来说就是经历过新生代垃圾回收后还存活下来的对象,容量通常比较大
2. 新老生代
3. 新生代垃圾回收
4. 老生代垃圾回收
5. 为什么需要分代式?
6. 并行回收(Parallel)
所谓并行,也就是同时的意思,它指的是垃圾回收器在主线程上执行的过程中,开启多个辅助线程,同时执行同样的回收工作
7. 增量标记与懒性清理
8. 并发回收(Concurrent)
它指的是主线程在执行 JavaScript 的过程中,辅助线程能够在后台完成执行垃圾回收的操作,辅助线程在执行垃圾回收的时候,主线程也可以自由执行而不会被挂起
博客
new 操作符具体干了什么呢?如何实现?
(1)首先创建了一个新的空对象
(2)设置原型,将对象的原型设置为函数的 prototype 对象。
(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
new共经历了四个过程。
1 |
|
JavaScript 原型,原型链? 有什么特点?
在 js 中我们是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。
当我们使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。
一般来说我们是不应该能够获取到这个值的,但是现在浏览器中都实现了__proto__
属性来让我们访问这个属性,但是我们最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个Object.getPrototypeOf() 方法,我们可以通过这个方法来获取对象的原型。
当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype所以这就是我们新建的对象为什么能够使用 toString() 等方法的原因。
2、隐式原型也是一个对象,是指向我们构造函数的原型
3、除了最顶层的Object对象没有__proto_,其他所有的对象都有__proto__,这是隐式原型
4、隐式原型__proto__的作用是让对象通过它来一直往上查找属性或方法,直到找到最顶层的Object的__proto__属性,它的值是null,这个查找的过程就是原型链
特点:
JavaScript 对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。
面试题–原型链的顶端是什么(如果找不到是什么)
在JavaScript中,每个对象都有一个原型(prototype)属性,指向它的原型对象。原型对象也有原型属性,指向它的原型对象,这样就形成了原型链。
原型链的顶端是Object.prototype,它是所有对象的祖先对象。Object.prototype的原型指向null,即它没有原型对象,所以当在原型链中找不到某个属性或方法时,就会返回undefined,而不是继续查找。
例如,当我们创建一个对象,并尝试访问它的属性或方法时,JavaScript引擎会先查找该对象本身是否具有该属性或方法,如果没有,则沿着原型链向上查找,直到找到该属性或方法或者到达Object.prototype为止。如果在整个原型链上都没有找到该属性或方法,那么就返回undefined。
总之,原型链的顶端是Object.prototype,它是所有对象的祖先对象,当在原型链中找不到某个属性或方法时,就会返回undefined。(这个答错了,回答的是null)
js 获取原型的方法?
p.__proto__
p.constructor.prototype
Object.getPrototypeOf(p)
hasOwnProperty表示是否有自己的属性。这个方法会查找一个对象是否有某个属性,但是不会去查找它的原型链
箭头函数和普通函数的区别
1.没有 this
箭头函数没有 this,所以需要通过查找作用域链来确定 this 的值。
这就意味着如果箭头函数被非箭头函数包含,this 绑定的就是最近一层非箭头函数的 this。
箭头函数this为父作用域的this,不是调用时的this
箭头函数的this永远指向其父作用域,任何方法都改变不了,包括call,apply,bind。
普通函数的this指向调用它的那个对象。
上例中,init为箭头函数,其内部的this为全局window,onclick的this也就是init函数的this,也是window,得到的this.name就为undefined。
2.没有 arguments (arguments是一个类数组对象,用来存储实际传递给函数的参数)
箭头函数没有自己的 arguments 对象,这不一定是件坏事,因为箭头函数可以访问外围函数的 arguments 对象:
那如果我们就是要访问箭头函数的参数呢?
你可以通过命名参数或者 rest 参数的形式访问参数:
let nums = (…nums) => nums;
…args剩余参数和 arguments对象的区别
剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参。
arguments对象不是一个真正的数组(类数组),而剩余参数是真正的 Array实例,也就是说你能够在它上面直接使用所有的数组方法,比如 sort,map,forEach或pop。
arguments对象还有一些附加的属性 (如callee属性)。
3.不能通过 new 关键字调用
JavaScript 函数有两个内部方法: [[Call]] 和 **[[Construct]]**。
当通过 new 调用函数时,执行 [[Construct]] 方法,创建一个实例对象,然后再执行函数体,将 this 绑定到实例上。
当直接调用的时候,执行 [[Call]] 方法,直接执行函数体。
箭头函数并没有 [[Construct]] 方法,不能被用作构造函数,如果通过 new 的方式调用,会报错。
1 |
|
4.没有 new.target
因为不能使用 new 调用,所以也没有 new.target 值。
关于 new.target,可以参考 es6
5.没有原型
由于不能使用 new 调用箭头函数,所以也没有构建原型的需求,于是箭头函数也不存在 prototype 这个属性。
1 |
|
解释——箭头函数为什么不能当做构造函数 (不能被实例化)
一句话解释:
因为箭头函数没有 [ [Construct] ] 方法
和ES6的类的概念有点像,如果一个类没有定义constructor,直接new也会报错
6.没有 super
连原型都没有,自然也不能通过 super 来访问原型的属性,所以箭头函数也是没有 super 的,不过跟 this、arguments、new.target 一样,这些值由外围最近一层非箭头函数决定。
箭头函数表达式的语法比函数表达式更短,并且不绑定自己的this,arguments,super或 new.target。这些函数表达式最适合用于非方法函数(non-method functions),并且它们不能用作构造函数
扩展(面试题)
类里面可以用箭头函数嘛
在JavaScript的类中,可以使用箭头函数,但是需要注意箭头函数的this指向。
在箭头函数中,this的值是在定义函数时确定的,而不是在运行时确定的。这意味着如果箭头函数中引用了this,它将指向箭头函数定义时的上下文,而不是类实例。
在大多数情况下,我们需要访问类实例中的属性和方法,因此在类中定义方法时通常使用传统函数语法而不是箭头函数。但是,在某些情况下,比如定义只读属性时,箭头函数可能更方便。
以下是一个例子,展示了在类中使用箭头函数的情况:
1 |
|
为什么有的时候可以用箭头函数有的时候不可以用
箭头函数与普通函数语法的一个主要区别是它们的this绑定方式不同。
在普通函数中,this的值是在运行时确定的,它取决于函数是如何调用的。this的值可以在函数调用时使用bind、call或apply方法进行修改。而在箭头函数中,this的值在函数定义时就已经确定,它始终指向箭头函数定义时所在的上下文,而不是调用时的上下文。
这就意味着,在某些情况下,箭头函数不适合使用。例如,在需要动态绑定this的情况下,箭头函数就不太合适。另外,在类的方法中,通常需要访问类实例的属性和方法,因此在这种情况下使用箭头函数可能会导致意外的行为。
另外一个需要注意的地方是,箭头函数不可以作为构造函数使用,因为它们没有自己的this绑定。在尝试使用箭头函数作为构造函数时,JavaScript会抛出一个TypeError错误。
因此,在选择使用箭头函数还是普通函数时,需要考虑到函数的用途和上下文,以确保正确的this绑定和预期的行为。
在类的方法中,通常需要访问类实例的属性和方法,因此在这种情况下使用箭头函数可能会导致意外的行为。代码示例
当在类中使用箭头函数作为方法时,this将会指向箭头函数定义时所在的上下文,而不是类实例。这可能会导致一些意外的行为,例如无法访问类实例的属性或方法。下面是一个例子:
1 |
|
在上面的例子中,我们使用箭头函数定义了一个名为printDetails的方法。当我们创建Person实例并调用该方法时,它会输出undefined,因为箭头函数的this指向Person类的定义上下文,而不是实例。因此,this.name和this.age都是undefined。此外,在类外部访问实例属性和方法时,它们仍然是可以访问的。
为了避免这种问题,我们通常使用普通函数语法定义类的方法,它们具有正确的this绑定,可以访问实例的属性和方法。例如:
1 |
|
在上面的例子中,我们使用普通函数定义了printDetails方法,它具有正确的this绑定,可以访问实例的属性和方法。在创建Person实例并调用该方法时,它会输出正确的实例属性值。
参考文章
This
谈一谈你对this的理解,以及在各种环境下的this
在浏览器里,在全局范围内this指向window对象
在函数中,this永远指向最后调用他的那个对象(箭头函数除外)。
在构造函数中,this指向new出来的新对象。
call、apply、bind中的this被强绑定在指定的那个对象上。
箭头函数this为父作用域的this,不是调用时的this。
博客:
call() 和 .apply() 的区别?
- 它们的作用一模一样,区别仅在于传入参数的形式的不同。
- call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。
1 |
|
apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。
bind 也能改变对象的执行上下文,它与 call 和 apply 不同的是,返回值是一个函数,并且需要稍后再调用一下,才会执行。
1 |
|
get 和 post 请求在缓存方面的区别
相关知识点:
get 请求类似于查找的过程,用户获取数据,可以不用每次都与数据库连接,所以可以使用缓存。
post 不同,post 做的一般是修改和删除的工作,所以必须与数据库交互,所以不能使用缓存。因此 get 请求适合于请求缓存。
回答:
缓存一般只适用于那些不会更新服务端数据的请求。一般 get 请求都是查找请求,不会对服务器资源数据造成修改
Get
GET请求的数据会附加在URL之后,用问号分割,多个参数用&进行连接。
GET请求的数据会暴露在地址栏中。
GET请求URL的编码格式采用的是ASCII编码,而不是Unicode编码。
GET请求传输大小有限制,大小在2KB。
GET相对安全性较差,会被浏览器主动缓存。
GET产生一个TCP数据包,head和data一起发送。
GET浏览器回退无害。
POST
POST请求会把数据放置在HTTP请求包的包体中,不会直接暴露给用户。
POST请求,理论上大小是不会限制的,但是实际上各个服务器会规定POST提交数据大小。
POST相对Get更安全,因为参数不会保存浏览器立式或者是web服务器日志中。
POST产生两个TCP数据包,header先发送,服务器响应100ms然后继续,发送data,服务器200然后返回数据。
POST浏览器回退重新请求。
其他http方法
setTimeout输出值的时候,如何实现i按序输出?
1 |
|
这道题挺经典的,输出结果是什么呢?结果是1000毫秒之后,输出5个5(隔一秒输出在1000上乘个i就行)
原因是,for循环在主线程内,setTimeout是异步方法,在任务队列里面,只有主线程执行完后,任务队列才执行,此时i的值已经是5,所以得到结果是5个5
那么怎么解决呢?其实思路很容易,只要每次循环把当前的i值传入setTimeout内即可
方法1:使用let
1 |
|
使用let 相当于每次循环的时候都新建了1个i并为其赋值
这是因为第一个代码块中setTimeout 的 console.log(i); 的i是 var 定义的,所以是函数级的作用域,不属于 for 循环体,属于 全局变量。等到 for 循环结束,i 已经等于 5 了,这个时候再执行 setTimeout 的五个回调函数(参考上面对事件机制的阐述),里面的 console.log(i); 的 i 去向上找作用域,只能找到 全局作用下 的 i,即 5。所以输出都是 5。
而let是代码块的作用域,即是局部变量,所以每一次 for 循环,console.log(i); 都引用到 for 代码块作用域下的i,因为这样被引用,所以 for 循环结束后,这些作用域在 setTimeout 未执行前都不会被释放。
方法2:定义函数并传值
1 |
|
方法3:IIFE(立即执行函数)
1 |
|
MDN
IIFE(立即调用函数表达式)
IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。
1 |
|
这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。第一部分是包围在 圆括号运算符 () 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。
第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。
方法4:使用闭包
1 |
|
方法5:setTimeout第三个参数传入i(是的,你没看错,setTimeout还有第三个参数)
1 |
|
正确定义的匿名函数
1 |
|
小括号的作用
小括号能把我们的表达式组合分块,并且每一块,也就是每一对小括号,都有一个返回值。这个返回值实际上也就是小括号中表达式的返回值。所以,当我们用一对小括号把匿名函数括起来的时候,实际上小括号返回的就是一个匿名函数的Function对象。因此,小括号对加上匿名函数就如同有名字的函数般被我们取得它的引用位置了。所以如果在这个引用变量后面再加上参数列表,就会实现普通函数的调用形式。 通俗点讲就是,加入小括号后就实现了和具名函数一样的形式
深拷贝和浅拷贝||赋值
相关资料:
1 |
|
- JS的基本类型不存在浅拷贝还是深拷贝的问题,深拷贝与浅拷贝的概念只存在于引用类型。
- 对于仅仅是复制了引用(地址),换句话说,复制了之后,原来的变量和新的变量指向同一个东西,彼此之间的操作会互相影响,为 浅拷贝。而如果是在堆中重新分配内存,拥有不同的地址,但是值是一样的,复制后的对象与原来的对象是完全隔离,互不影响,为 深拷贝。
利用{…}(剩余运算符,不定参数)实现拷贝。注意这里是拷贝,先不区分是深还是浅
如果是引用类型,且只有一层对象,则这一层就是深拷贝。拷贝前后,对象的基本类型互不影响,
如果是引用类型,且有两层以上嵌套对象,则这嵌套的,就是浅拷贝。嵌套的对象,拷贝的嵌套对象的引用。拷贝前后,嵌套对象数据相互影响,一层对象,互不影响
掘金
- 深浅拷贝 的主要区别就是:复制的是引用(地址)还是复制的是实例。
浅拷贝指的是将一个对象的属性值复制到另一个对象,如果有的属性的值为引用类型的话,那么会将这个引用的地址复制给对象,因此两个对象会有同一个引用类型的引用。浅拷贝可以使用Object.assign 和展开运算符来实现。
深拷贝相对浅拷贝而言,如果遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象可以使用 JSON 的两个函数来实现,但是由于 JSON 的对象格式比 js 的对象格式更加严格,所以如果属性值里边出现函数或者 Symbol 类型的值时,会转换失败。
博客:
浅拷贝的实现
- Object.assign()
- Array.prototype.concat()
- Array.prototype.slice()
- 展开运算符…
- 函数库lodash的_.clone方法
深拷贝的实现方式
- JSON.parse()
- JSON.stringify()
- 函数库lodash的_.cloneDeep方法
- jQuery.extend()方法
- 手写递归方法
参考
掘金
赋值
- 当我们把一个变量赋值给另一个变量时,是在栈内存中新增一个变量,并且赋值。二者互不影响
1 |
|
- 当我们把一个对象赋值给一个新的对象时,赋的是该对象在栈中的地址,而不是堆中的数据,也就是赋值之后,两个对象指向同一个存储空间。无论哪个对象发生改变,其实都是改变的堆中的数据(会互相影响,指向的引用地址都相同)
1 |
|
slice(不会改变原始数组) 和 concat(不会更改现有数组,而是返回一个新数组)
Array 的 slice 和 concat 方法 和 jQuery 中的 extend 复制方法,他们都会复制第一层的值,对于 第一层 的值都是 深拷贝,而到 第二层 的时候 Array 的 slice 和 concat 方法就是 复制引用 ,jQuery 中的 extend 复制方法 则 取决于 你的 第一个参数, 也就是是否进行递归复制。所谓第一层 就是 key 所对应的 value 值是基本数据类型,也就像上面栗子中的name、age,而对于 value 值是引用类型 则为第二层,也就像上面栗子中的 company。
ES6
ES6 语法知道哪些,分别怎么用?
let const 块级作用域 箭头函数 词法this Class 解构,剩余运算符,Promise等,往这些方面展开。 不定参数…rest
let与var对比(做项目中习惯用let声明变量)
var声明的变量往往会越狱 (let 是在代码块内有效,var 是在全局范围内有效:)
let声明的变量有严格局部作用域
var可以声明多次,let只能声明一次
let 不存在变量提升var 会变量提升 (undefined)
const声明变量 (只读变量)
声明之后不允许改变
一旦声明必须初始化,否者会报错
const 如何做到变量在声明初始化之后不允许改变的?其实 const 其实保证的不是变量的值不变,而是保证变量指向的内存地址所保存的数据不允许改动
解释:
对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
Set Map Reduce
Set类似于数组,但是它里面每一项的值是唯一的,没有重复的值,Set是一个构造函数,用来生成set的数据结构
Set中两个对象永远是不相等的,即使键和值都是一样的
Map的特点?Map类似于对象,也是键值对的集合
方法(1. set2. get3. has4. delete5. clear)
6. Map默认情况下不包含任何键,所有键都是开发人员添加进去的。不像Object原型链上有一些默认的键。
7. Map的键可以是任意类型数据,就连函数都可以。Object的键只能是String或Symbol。
8. Map的键值对个数可以轻易通过size属性获取,Object需要手动计算。
9. Map在频繁增删键值对的场景下性能要比Object好。
什么时候用Map?
要添加的键值名和Object上的默认键值名冲突,又不想改名时,可以换用Map。 JS对象自己的对象属性可能会与从原型继承的属性键冲突(例如 toString,constructor等)。
需要String和Symbol以外的数据类型做键值时,用Map。
键值对很多,有需要计算数量时,用Map。
需要频繁增删键值对时,用Map。
Map()和map是不一样的
reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
博客:
Map 和 WeakMap 的区别
1.Map 的键可以是任意类型,WeakMap 只接受对象作为键(null除外),不接受其他类型的值作为键
- Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键; WeakMap 的键是弱引用,键所指向的对象可以被垃圾回收,此时键是无效的
- Map 可以被遍历, WeakMap 不能被遍历
WeakMap的使用场景(Map 和 Weakmap 之间的主要区别是,Weakmap 允许对键对象进行垃圾收集,从而防止内存泄漏。)
- DOM 节点元数据
因为 weakMap 不会影响垃圾回收,所以可以用来关联元数据 - 部署私有属性
利用弱映射,将内部属性设置为实例的弱引用对象,当实例删除时,私有属性也会随之消失,因此不会内存泄漏 - 数据缓存
当我们需要在不修改原有对象的情况下储存某些属性等,而又不想管理这些数据时,可以使用WeakMap
博客
浅析 Map 和 WeakMap 区别以及使用场景
模块化
模块化就是把代码进行拆分,方便重复利用,类似java中的导包,需要使用一个包,必须先导包,而js中没有包的概念,换来的是模块
ES6 的模块化分为导出(export) @与导入(import)两个模块。
模块化优点
避免命名冲突(减少命名空间污染)更好的分类,按需加载,更高的复用性,高可维护性
CommomJS(node应用由模块组成)
CommonJS定义了两个主要概念:
require函数,用于导入模块
module.exports变量,用于导出模块
require导入,代码很简单,let {count,addCount}=require(“./utils”)就可以了。
require的第一步是解析路径获取到模块内容:
ES6模块与CommonJS的区别
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
- CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
- ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。
- ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
- 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
- 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。
CommonJs导出有有缓存,而ES6没有
ECMAScript6 怎么写 class,为什么会出现 class 这种东西?
在我看来 ES6 新添加的 class 只是为了补充 js 中缺少的一些面向对象语言的特性,但本质上来说它只是一种语法糖,不是一个新的东西,其背后还是原型继承的思想。通过加入 class 可以有利于我们更好的组织代码。在 class 中添加的方法,其实是添加在类的原型上的。
数组&&对象 方法(基础)
数组去重 (indexOf set map for Object键值对)
法一:indexOf循环去重
法二:ES6 Set去重;Array.from(new Set(array))
法三:Object 键值对去重;把数组的值存成 Object 的 key 值,比如 Object[value1] = true,在判断另一个值的时候,如果 Object[value2]存在的话,就说明该值是重复的。
掘金
数组常用方法(一般方法)
push(),pop(),shift(),unshift(),splice(),sort(),reverse(),map()等
会改变原始数组的方法有
1 |
|
不改变原始数组的方法有
1 |
|
数组和对象方法(进阶)
indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置。如果没有找到匹配的字符串则返回 -1。
find(返回数组中满足条件的第一个元素的值,如果没有,返回undefined)
array.findeIndex(callback[,thisArg]) 返回数组中满足条件的第一个元素的下标,如果没有找到,返回**-1**
数组常用的方法 find filter every some
判断一个数组中有符合我需求的元素用哪个比较合适–indexof find
如果要返回一个布尔值— includes some every
js字符串数组互转
字符串转数组
- split() 方法 将字符串转换成一个数组
- 扩展运算符[…] es6里面的扩展运算符
- Array.from()方法 es6 Array.from()方法
1 |
|
数组转字符串
toString()方法 将数组转换成一个字符串
toLocaleString()方法 把数组转换成本地约定的字符串
join()方法 将数组元素连接起来以构建一个字符串
1 |
|
JS如何实现数组扁平化?
- ES6的flat
1 |
|
- 正则处理
- 递归
1 |
|
- reduce
- toString
JS求数组的交集、并集、差集
现有两个数组
1 |
|
求交集(交集元素由既属于集合a又属于集合b的元素组成)
- 方法1 filter includes()
1 |
|
- 方法2 filter indexOf()
1 |
|
- 方法3 filter Set() set.has
1 |
|
- 方法4
1 |
|
2.求并集 (并集元素由集合a和集合b中所有元素去重组成)
- 方法1 filter includes()
1 |
|
- 方法2 Array.from() Set() concat() (最简单)
1 |
|
- 方法3 concat() filter() indexOf()
1 |
|
对象常用的方法–遍历一个对象 for in Object.keys Object.values Object.entries
工作中遍历对象都用for in 嘛,遍历用的比较多的方法是(有用过forEach嘛)
ES6 Object新增的Api
-keys(obj),获取对象的所有key形成的数组
-values(obj),获取对象的所有value形成的数组
-entries(obj),获取对象的所有key和value形成的二维数组
Object.assign(dest, ···src) ,将多个src对象的值拷贝到dest中(第一层为深拷贝,第二层为浅拷贝)
Object.is()
__proto__属性
对象拓展运算符 (…)第一层为深拷贝,第二层为浅拷贝
博客
for in和for of 区别
for in
我们直接从一段代码来看
1 |
|
有哪些缺陷呢👇
index获取的是索引
遍历的顺序可能不是按照顺序进行的
使用for in 会遍历数组所有可枚举属性,包括原型。例如上面的method和name都会遍历
for in 更适合遍历对象,不要使用for in去遍历数组
for of
1 |
|
for of语法遍历的是数组元素的值
for in 遍历的是索引
for of遍历的只是数组内的元素(value),而不包括数组的原型属性method和索引name
小结
for in 可以遍历一个普通的对象,这样也是它的本质工作,for in会遍历原型以及可枚举属性,最好的情况下,使用hasOwnProperty判断是不是实例属性。
for..of适用遍历数/数组对象/字符串/map/set等拥有迭代器对象的集合,不能遍历对象,因为没有迭代对象,与forEach()不同的是,它可以正确响应break、continue和return语句。
如何使用for of 循环遍历对象(给对象添加迭代器)
- 类数组对象
如果对象是一个类数组对象,那好办,用Array.from转成数组即可。 - 非类数组对象
如果不是类数组对象,也有办法,添加一个[Symbol.iterator]属性,并指向一个迭代器即可。
Object.keys/Object.values/Object.entries
任何数据结构只要部署 Iterator 接口,就可以完成遍历操作,即依次处理该数据结构的所有成员
迭代器对象本质上,就是一个指针对象。通过指针对象的next(),用来移动指针。
【迭代器协议】对象必须提供一个next(),执行该方法要么返回迭代中的下一项,要么就引起一个Stopiteration异常,以终止迭代。
next()返回一个对象,表示当前数据成员的信息。这个对象具有value和done两个属性,value属性返回当前位置的成员,done属性是一个布尔值,表示遍历是否结束,即是否还有必要再一次调用next()。对于遍历器对象来说,done: false和value: undefined属性都是可以省略的。
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性上。
如何判断一个类型是不是可迭代对象
1 |
|
常见的可迭代对象,有Array,Map, Set, String,TypeArray, arguments
可以通过判断Symbol.iterator判断当前变量是否是可迭代对象
forEach(数组方法)首先forEach是不能使用任何手段跳出循环的
特性:
便利的时候更加简洁,效率和for循环相同,不用关心集合下标的问题,减少了出错的概率。
没有返回值
不能使用break中断循环,不能使用return返回到外层函数
1 |
|
注意:
- forEach() 对于空数组是不会执行回调函数的。
- for可以用continue跳过循环中的一个迭代,forEach用continue会报错。
- forEach() 需要用 return 跳过循环中的一个迭代,跳过之后会执行下一个迭代。
forEach map(ES5)的区别
能用forEach()做到的,map()同样可以。反过来也是如此
map()会分配内存空间存储新数组并返回,forEach()不会返回数据。
forEach()允许callback更改原始数组的元素。map()返回新的数组。 map() 不会改变原始数组。
map() forEach() (数据为基本类型,改变其值(不会改变))(数据为引用类型,改变其属性方法的值(会改变))
map() 可以利用 return 语句来返回值,该值将被放入新数组中。forEach() 方法不能使用 return 语句返回值。
map和forEach是按 (value,key)的方式遍历的
1 |
|
mdn –forEach
mdn–map
js学习-forEach和map是否改变原数组?
基础 (已掌握)
js种8种数据类型
六种基本数据类型:undefined,null,number,bollean, string
还有在 ES6 中新增的 Symbol 和 ES10 中新增的 BigInt 类型。
Symbol 代表创建后独一无二且不可变的数据类型,它的出现我认为主要是为了解决可能出现的全局变量冲突的问题。
BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
另一种是复杂数据类型:Object
js 可以分为两种类型的值,一种是基本数据类型,一种是复杂数据类型。
复杂数据类型指的是 Object 类型,所有其他的如 又称为引用类型,包括 Array Date function 等数据类型都可以理解为 Object 类型的子类。
两种类型间的主要区别是它们的存储位置不同,基本数据类型的值直接保存在栈中,而复杂数据类型的值保存在堆中,通过使用在栈中保存对应的指针来获取堆中的值
js判断类型
判断方法:typeof(),instanceof,constructor,Object.prototype.toString.call()等
1. typeof 其中数组、对象、null都会被判断为Object,其他判断都正确
返回数据类型,包含这7种: number、boolean、symbol、string、object、undefined、function。typeof null 返回类型错误,返回object ,引用类型,除了function返回function类型外,其他均返回object。其中,null 有属于自己的数据类型 Null
2. instanceof 只能判断引用数据类型,不能判断基本数据类型
1 |
|
instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型,
由上图可以看出[]的原型指向Array.prototype,间接指向Object.prototype, 因此 [] instanceof Array 返回true, [] instanceof Object 也返回true。
instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。
3. constructor(2个作用 一是判断数据的类型,二是对象实例通过constructor对象访问它的构造函数。需要注意的事情是如果创建一个对象来改变它的原型,constructor就不能来判断数据类型了)
1 |
|
constructor是原型prototype的一个属性,当函数被定义时候,js引擎会为函数添加原型prototype,并且这个prototype中constructor属性指向函数引用, 因此重写prototype会丢失原来的constructor。
不过这种方法有问题:
1:null 和 undefined 无constructor,这种方法判断不了。
2:还有,如果自定义对象,开发者重写prototype之后,原有的constructor会丢失,因此,为了规范开发,在重写对象原型时一般都需要重新给 constructor 赋值,以保证对象实例的类型不被篡改。
4. toString 这个是最完美的
toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。
对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。
原理
在toString方法被调用时,会执行下面的操作步骤:
获取this对象的[[Class]]属性的值.
计算出三个字符串”[object “, 第一步的操作结果Result(1), 以及 “]”连接后的新字符串.
返回第二步的操作结果Result(2).
判断类型举例:
1 |
|
如何判断一个对象是数组还是对象
一、typeof判断数据类型(判断数组跟对象都返回object)
二、instanceof判断对象的原型链是否是指向构造函数的prototype
1 |
|
三、对象的constructor属性
1 |
|
四、Object.prototype.toString.call(arr)
利用对象的toString可以准确判断是什么类型,call()改变this指向,这里是借用Object的方法,然后有人可能会问为什么不直接用arr.toString而要借用Object的方法,
1 |
|
直接用tostring()
1 |
|
因为toString为Object原型上的方法,而Array、Function都是Object的实例,实例重新改写了原型上的toString方法,不同的对象调用toString方法,调用的是改写之后的方法
(转成各种类型的字符串),而不会调用Object原型上的toString()方法,因此直接调用不能判断对象类型
1 |
|
我们可以看到,删除实例上的toString方法后调用的是Object原型上的toString()方法,返回对象类型
五、es6的方法——Array.isArray()
1 |
|
六、看好多博客说用length判断,这个其实不准确
1 |
|
对象可以直接添加length这个属性,就无法区分了
暂停死区
在代码块内,使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”
较陌生不常问(需掌握)
js 延迟加载的方式有哪些?
js 延迟加载,也就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度
一般有以下几种方式:(defer 属性async 属性动态创建 DOM 方式使用 setTimeout 延迟方法 让 JS 最后加载)
js 的加载、解析和执行会阻塞页面的渲染过程,因此我们希望 js 脚本能够尽可能的延迟加载,提高页面的渲染速度。
我了解到的几种方式是:
第一种方式是我们一般采用的是将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。
第二种方式是给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
第三种方式是给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
第四种方式是动态创建 DOM 标签的方式,我们可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
0.1+0.2为什么不等于0.3?
当计算机计算 0.1+0.2 的时候,实际上计算的是这两个数字在计算机里所存储的二进制,0.1 和 0.2 在转换为二进制表示的时候会出现位数无限循环的情况。js 中是以 64 位双精度格式来存储数字的,只有 53 位的有效数字,超过这个长度的位数会被截取掉这样就造成了精度丢失的问题。这是第一个会造成精度丢失的地方。在对两个以 64 位双精度格式的数据进行计算的时候,首先会进行对阶的处理,对阶指的是将阶码对齐,也就是将小数点的位置对齐后,再进行计算,一般是小阶向大阶对齐,因此小阶的数在对齐的过程中,有效数字会向右移动,移动后超过有效位数的位会被截取掉,这是第二个可能会出现精度丢失的地方。当两个数据阶码对齐后,进行相加运算后,得到的结果可能会超过 53 位有效数字,因此超过的位数也会被截取掉,这是可能发生精度丢失的第三个地方。
对于这样的情况,我们可以将其转换为整数后再进行运算,运算后再转换为对应的小数,以这种方式来解决这个问题。
(toPrecision vs toFixed –toPrecision 是处理精度,精度是从左至右第一个不为0的数开始数起。
–toFixed 是小数点后指定位数取整,从小数点开始数起。)
我们还可以将两个数相加的结果和右边相减,如果相减的结果小于一个极小数,那么我们就可以认定结果是相等的,这个极小数可以
使用 es6 的 Number.EPSILON
Node 环境中的事件环(Event Loop)
Node是基于V8引擎的运行在服务端的JavaScript运行环境,在处理高并发、I/O密集(文件操作、网络操作、数据库操作等)场景有明显的优势。虽然用到也是V8引擎,但由于服务目的和环境不同,导致了它的API与原生JS有些区别,其Event Loop还要处理一些I/O,比如新的网络连接等,所以Node的Event Loop(事件环机制)与浏览器的是不太一样。
执行顺序如下:
timers: 计时器,执行setTimeout和setInterval的回调
pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
idle, prepare: 队列的移动,仅系统内部使用
poll轮询: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。
check: 执行setImmediate回调,setImmediate在这里执行
close callbacks: 执行close事件的callback,一些关闭的回调函数,如:socket.on(‘close’, …)
哪些操作会造成内存泄漏?
相关知识点:
1.意外的全局变量
2.被遗忘的计时器或回调函数
3.脱离 DOM 的引用
4.闭包
第一种情况是我们由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
第二种情况是我们设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留
在内存中,而无法被回收。
第三种情况是我们获取一个 DOM 元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回
收。
第四种情况是不合理的使用闭包,从而导致某些变量一直被留在内存当中。
作用域和作用域链
创建函数的时候,已经声明了当前函数的作用域==>当前创建函数所处的上下文。如果是在全局下创建的函数就是[[scope]]:EC(G),函数执行的时候,形成一个全新的私有上下文EC(FN),供字符串代码执行(进栈执行)
定义:简单来说作用域就是变量与函数的可访问范围,由当前环境与上层环境的一系列变量对象组成
1.全局作用域:代码在程序的任何地方都能被访问,window 对象的内置属性都拥有全局作用域。
2.函数作用域:在固定的代码片段才能被访问
作用:作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
作用域链参考链接一般情况下,变量到 创建该变量 的函数的作用域中取值。但是如果在当前作用域中没有查到,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。
掘金
什么是 DOM 和 BOM?
DOM 指的是文档对象模型,它指的是把文档当做一个对象来对待,这个对象主要定义了处理网页内容的方法和接口。
BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口。
BOM的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。
博客:
事件冒泡
事件会从最内层的元素开始发生,一直向上传播,直到document对象。
1 |
|
因此上面的例子在事件冒泡的概念下发生click事件的顺序应该是
1 |
|
事件捕获
与事件冒泡相反,事件会从最外层开始发生,直到最具体的元素。
上面的例子在事件捕获的概念下发生click事件的顺序应该是
1 |
|
addEventListener 事件 捕获和冒泡(应用场景:事件委托)
addEventListener方法用来为一个特定的元素绑定一个事件处理函数,是JavaScript中的常用方法。
1 |
|
重点来看看第三个参数useCapture
true - 事件句柄在捕获阶段执行(即在事件捕获阶段调用处理函数)
false- false- 默认。事件句柄在冒泡阶段执行(即表示在事件冒泡的阶段调用事件处理函数)
所以我们通常来说,默认第三个参数不写的话,是按照事件句柄在冒泡执行的。
attachEvent (兼容IE的写法)
默认是事件冒泡阶段调用处理函数,写事件名时候要加上”on”前缀(”onload”、”onclick”等)。
1 |
|
事件代理
利用事件流的特性,我们可以使用一种叫做事件代理的方法,其实利用的就是事件冒泡的机制。
1 |
|
js代码
1 |
|
更加规范的写法👇
1 |
|
事件委托如何获取哪个子节点触发的函数
事件委托所有子元素都会触发点击事件,所以给需要监听的元素设置一个相同的类名或者相同的标签,先判断点击的元素是否为需要触发事件的标签。然后再进行相应的判断
1 |
|
阻止事件冒泡和默认事件(event.preventDefault() // 阻止默认事件&&event.stopPropagation() //阻止冒泡)
全局主题切换如何实现
几种方法
基于css变量实现,高效快捷。
动态切换根元素样式名或ID。
element官方动态主题色 + 动态切换文件(要兼容IE低版本,最终决定使用此方法)
掘金
三步实现主题切换
typeof NaN 的结果是什么?
NaN 意指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出
数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。
1 |
|
NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN != NaN为 true。
null是对象吗?为什么?
1 |
|
结论: null不是对象。
解释: 虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。
在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存
null和undefined的区别
null和undefined的区别(简单易懂)
null==undefined结果为true
null===undefined结果为false
事件是什么?IE 与火狐的事件机制有什么区别? 如何阻止冒泡?
1.事件是用户操作网页时发生的交互动作,比如 click/move, 事件除了用户触发的动作外,还可以是文档加载,窗口滚动和大小调整。事件被封装成一个 event 对象,包含了该事件发生时的所有相关信息( event 的属性)以及可以对事件进行的操作( event 的方法)。
2.事件处理机制:IE 支持事件冒泡、Firefox 同时支持两种事件模型,也就是:事件冒泡和事件捕获。
3.event.stopPropagation() 或者 ie 下的方法 event.cancelBubble = true;
详细资料可以参考:
《Javascript 事件模型系列(一)事件及事件的三种模型》
《Javascript 事件模型:事件捕获和事件冒泡》
~ 操作符的作用?~x 大致等同于 -(x+1)
~ 返回 2 的补码,并且 ~ 会将数字转换为 32 位整数,因此我们可以使用 ~ 来进行取整操作。
{} 和 [] 的 valueOf 和 toString 的结果是什么?
{} 的 valueOf 结果为 {} ,toString 的结果为 “[object Object]”
[] 的 valueOf 结果为 [] ,toString 的结果为 “”
mouseover和mouseenter的区别
mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡的过程。对应的移除事件是mouseout
mouseenter:当鼠标移除元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是mouseleave
去除字符串首尾空格
- 使用正则(^\s*)|(\s*$)即可
- 使用jquery
1 |
|
比较运算符!!!重要
= 赋值
== 等于(类型不一样,值一样,也会判断为true)
=== 绝对等于 (类型一样,值一样,结果为true)
Object.is()使用过吗?跟 === 和 == 区别
两等号判等,会在比较时进行类型转换。
三等号判等(判断严格),比较时不进行隐式类型转换,(类型不同则会返回false)
使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 认定为是相等的。
其他不常见
js如何将json字符串转换为json对象(SON 是轻量级的文本数据交换格式)
JSON字符串:
var str1 = ‘{ “name”: “cxh”, “sex”: “man” }’;
JSON对象:
var str2 = { “name”: “cxh”, “sex”: “man” };
一、JSON字符串转换为JSON对象 (eval(()函数 &&parseJSON && parse)
二、可以使用toJSONString()或者全局方法JSON.stringify()将JSON对象转化为JSON字符串
例如:
var last=obj.toJSONString(); //将JSON对象转化为JSON字符
或者
var last=JSON.stringify(obj); //将JSON对象转化为JSON字符
Other
number与string的转换(面试题)
number -> string
1 |
|
string -> number
1 |
|
装箱与拆箱
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!